package roborallyimplementation;

import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class used to represent positions on a particular board.
 * 
 * @invar	This position's coordinates are positive numbers.
 * 			|	this.getX() >= 0 && this.getY() >= 0
 * @author Thomas Vochten (1ba inform.), Daan Daele (1ba inform.)
 *
 */

public class Position {

	/**
	 * Initializes a position with the given x-coordinate and y-coordinate.
	 * 
	 * @param	x
	 * 			The x-coordinate of the new position.
	 * @param	y
	 * 			The y-coordinate of the new position.
	 * @pre		Both coordinates are positive coordinates.
	 * 			|	x >= 0 && y >= 0
	 * @post	The x-coordinate and y-coordinate of the new board equal the given x-coordinate and y-coordinate.
	 * 			|	new.xcoord == x && new.ycoord == y
	 */
	@Raw
	public Position(long x, long y)
	{
		this.xcoord = x;
		this.ycoord = y;
	}
	
	/**
	 * Compares this position to the given object. The object is assumed to be an effective position.
	 * If not, an exception is thrown.
	 * 
	 * @param	object
	 * 			The object that this position will be compared to.
	 * @return	True if and only if object is an effective object of class Position and its coordinates are equal.
	 * 			|	if (object == null)
	 * 			|		then result == false
	 * 			|	if (! (object instanceof Position))
	 * 			|		then result == false
	 * 			|	result == object.getX() == this.getX() && object.getY() == this.getY()
	 */
	@Override
	public boolean equals(Object object)
	{
		if (object == null)
			return false;
		if (! (object instanceof Position))
			return false;
		return ((Position) object).getX() == this.getX() && ((Position) object).getY() == this.getY();
	}
	
	/**
	 * Returns a new position object that is next to the current position based on the given orientation.
	 * 
	 * @param	orientation
	 * 			The orientation to be considered.
	 * @return	A new position object that is next to the current position.
	 * 			|	result == new Position(this.getX() + calculateDirection(orientation)[0], this.getY() +
	 * 				calculateDirection(orientation)[1]
	 */
	public Position getNeighbour(int orientation)
	{
		long[] direction = calculateDirection(orientation);
		return new Position(this.getX() + direction[0], this.getY() + direction[1]);
	}
	
	/**
	 * Gets the x-coordinate of this position.
	 */
	@Basic
	public long getX()
	{
		return xcoord;
	}
	
	/**
	 * Gets the y-coordinate of this position.
	 */
	@Basic
	public long getY()
	{
		return ycoord;
	}
	
	/**
	 * Calculates the manhattan distance between this position and the other position.
	 * 
	 * @param	other
	 * 			The other position to be used in the calculation.
	 * @return	The manhattan distance equals the sum of the absolute value of the difference
	 * 			between the x-coordinates and the y-coordinates of both positions.
	 * 			|	result == Math.abs(this.getX() - other.getX()) + Math.abs(this.getY() - other.getY())
	 */
	public long getManhattanDistance(Position other)
	{
		return Math.abs(this.getX() - other.getX()) + Math.abs(this.getY() - other.getY());
	}
	
	/**
	 * Helper method for getNeighbour() and isPlacedAtOrientationOf(): converts the given orientation
	 * to a vector.
	 * 
	 * @param	orientation
	 * 			The orientation that shall be converted.
	 * @return	An array of length two that represents the vector: only one of the components will not equal zero.
	 */
	@Model
	private long[] calculateDirection(int orientation)
	{
		long[] toreturn = {0, 0};
		if (orientation == 0)
			toreturn[0] = 1;
		if (orientation == 90)
			toreturn[1] = -1;
		if (orientation == 180)
			toreturn[0] = -1;
		if (orientation == 270)
			toreturn[1] = 1;
		return toreturn;
	}
	
	private final long xcoord;
	private final long ycoord;
	
	/**
	 * Returns all elements that have been placed on this position.
	 */
	@Basic
	public List<Element> getElements()
	{
		return placedHere;
	}
	
	/**
	 * Determines whether this position can have the given element placed on it.
	 * 
	 * @param	element
	 * 			The element to be considered.
	 * @return	True if and only if the element is an effective element and this position
	 * 			does not already contain the given element and all elements on this position can share a
	 * 			position with the given element, false otherwise.
	 * 			|	result == ((element != null) && (! this.getElements().contains(element)))
	 * 				&& for each element2 in this.getElements(): element2.canSharePositionWith(element)
	 */
	public boolean canAddThisElement(Element element)
	{
		if ((element == null) || (this.getElements().contains(element)))
			return false;
		if (this.getElements().isEmpty())
			return true;
		boolean returnValue = true;
		for (Element element2 : this.getElements())
		{
			if (! element2.canSharePositionWith(element))
				returnValue = false;
		}
		return returnValue;
	}
	
	/**
	 * Places an element on this position.
	 * 
	 * @param	element
	 * 			The element to be added.
	 * @post	The given element will have been placed on this position.
	 * 			|	new.getElements.contains(element)
	 * @throws	IllegalArgumentException
	 * 			The given element cannot be placed on this position.
	 * 			|	! canAddThisElement(element)
	 * @throws	IllegalPositionException
	 * 			The given element cannot have this position as position.
	 * 			|	! element.canHaveAsPosition(this)
	 */
	public void placeElement(Element element) throws IllegalArgumentException, IllegalPositionException
	{
		if (! canAddThisElement(element))
			throw new IllegalArgumentException();
		element.setPosition(this);
		placedHere.add(element);
	}
	
	/**
	 * Removes the given element from this position.
	 * 
	 * @param	element
	 * 			The element to be removed.
	 * @post	The given element will have been removed from this position if it was present
	 * 			on this position.
	 * 			|	if (this.getElements().contains(element)) then ! new.getElements().contains(element)
	 */
	public void removeElement(Element element)
	{
		placedHere.remove(element);
	}
	
	private List<Element> placedHere = new ArrayList<Element>();
	
	/**
	 * Calculates the hash code corresponding to this position.
	 * 
	 * @return	Initially, the hash code equals one. Then, both coordinates are multiplied by 31
	 * 			and subsequently get added to the initial value.
	 * 			|	result == 1 + this.getX() * 31 + this.getY() * 31
	 */
	@Override @Basic
	public int hashCode()
	{
		int hash = 1;
		hash = (int) (hash + this.getX() * 31);
		hash = (int) (hash + this.getY() * 31);
		return hash;
	}
	
	/**
	 * Calculates whether this position is next to the other position considering the given orientation.
	 * 
	 * @param	position
	 * 			The position that will be used in the calculation.
	 * @param	orientation
	 * 			The orientation to be considered.
	 * @return	This position equals the position created by the getNeighbour() method if called with
	 * 			the given orientation as a parameter.
	 * 			|	result == this.equals(position.getNeighbour(orientation))
	 */
	public boolean isPlacedAtOrientationOf(Position position, int orientation)
	{
		return this.equals(position.getNeighbour(orientation));
	}
	
	/**
	 * Returns a string representation of this position.
	 * 
	 * @return	The string representation.
	 */
	public String toString()
	{
		return "x-coordinate = " + this.getX() + "; y-coordinate = " + this.getY();
	}
	
	/**
	 * Returns the coordinate pair that corresponds to this position.
	 * 
	 * @return	The coordinate pair.
	 */
	public CoordinatePair toCoordinatePair()
	{
		return new CoordinatePair(this.getX(), this.getY());
	}
}
